gl-sdk: Add LNURL-auth (LUD-04 / LUD-05) and unify Node entry points#708
gl-sdk: Add LNURL-auth (LUD-04 / LUD-05) and unify Node entry points#708angelix wants to merge 2 commits intoave-lnurl-improvementsfrom
Conversation
parse_input is now a single async entry point that resolves LNURL bech32 strings and Lightning Addresses end-to-end over HTTP, returning typed pay or withdraw request data. BOLT11 invoices and node IDs still resolve without I/O. Mobile callers get one async call, one error path, one loading state. InputType variants are now Bolt11, NodeId, LnUrlPay, LnUrlWithdraw — the intermediate LnUrl / LnUrlAddress states are gone from the public surface. Node::resolve_lnurl and the ResolvedLnUrl enum are removed; callers obtain LnUrlPayRequestData / LnUrlWithdrawRequestData from parse_input and pass them to Node::lnurl_pay / Node::lnurl_withdraw. Also adds the parse_input async wrapper to the napi (Node.js) binding so JS callers regain LNURL resolution after Node::resolve_lnurl removal, plus a jest spec covering BOLT11 / NodeId pass-through and error-before-HTTP cases. Tests updated: gl-sdk Rust unit tests cover the network-free paths; gl-sdk Python integration tests in test_lnurl.py and test_parse_input.py exercise the LNURL service fixture; gl-sdk-android tests run under runBlocking against the suspend fun parseInput. Python bindings regenerated.
Adds Node::lnurl_auth(request) implementing LUD-04 / LUD-05. The
LUD-05 BIP32 namespace xpriv at m/138' is derived once from the BIP39
seed at register/recover/connect time and stored on Node in
Zeroizing<Vec<u8>>; the seed itself never persists. Because m/138' is
hardened, the stored material cannot derive any other wallet key
(lightning-channel keys, on-chain wallet) — the blast radius is
restricted to LNURL-auth identities. disconnect() and Drop for Node
both take() the option to scrub eagerly. The LUD-05 HMAC key is the
32-byte private key at m/138'/0, matching the mainstream wallet
convention (Phoenix, Mutiny, Zeus, BlueWallet) for cross-wallet
identity portability — locked in by a known-vector test against the
"abandon...about" BIP39 mnemonic at example.com.
InputType gains an LnUrlAuth { data: LnUrlAuthRequestData } variant.
Detection is offline: tag=login URLs are classified from the URL
query string without an HTTP fetch. New types LnUrlAuthRequestData
and LnUrlCallbackStatus join the existing LnUrl* family.
Removes the bare Node(credentials) constructor from both UniFFI and
napi binding surfaces — there are now zero public Node constructors.
The crate-private constructor is renamed with_signer → new
(pub(crate)) and now requires lnurl_auth_xpriv: Zeroizing<Vec<u8>>
at construction; only disconnect()/Drop produce None thereafter, and
lnurl_auth errors with "LNURL-auth namespace key has been scrubbed"
on the post-scrub path. Four uniform entry points across all
bindings: register / recover / connect / register_or_recover. The
napi binding gains those free fns plus a Config wrapper (withNetwork,
withDeveloperCert) and Node.disconnect() / Node.credentials() — JS
callers no longer manage an external Signer/Handle.
Tests: 28 Rust unit tests cover offline classification, LUD-05
derivation determinism, signature verification against the derived
xpub, callback-status JSON parsing, and the fixed pubkey vector. The
LNURL fixture (gl-testing/lnurl_server.py) gains an /auth route with
ECDSA verification via coincurve. Python integration tests cover
classification-without-HTTP, the end-to-end auth flow, deterministic
per-domain pubkey, and the disconnect-scrubs-key contract. The two
LNURL-pay end-to-end tests are un-skipped via a _bip39_seed helper
that aligns the gl-client clients fixture seed format with
glsdk.connect's mnemonic-derived 64-byte seed (gl-testing's defensive
len(secret) == 32 assert relaxed to (32, 64) accordingly). Android
gains 5 new offline LnUrlAuth parse cases.
napi tests rewritten to use the new register / recover / connect free
fns instead of new Node(credentials); the README's quickstart sample
follows suit.
cdecker
left a comment
There was a problem hiding this comment.
No, the signer is not guaranteed to be at the same location. If we want to have lnurl-auth we will need to run it through the CLN node, where we may have a signer present.
The reason for wanting signerless clients is because the signer is equivalent to root access to the node, i.e., the signer can self-certify any runes and TLS certs to escalate whatever privileges. We cannot assume the signer is present, and any operation that requires signing something, must involve the node.
| /// Canonical constructor. | ||
| /// | ||
| /// Crate-private — UniFFI consumers reach this via the top-level | ||
| /// `register` / `recover` / `connect` / `register_or_recover` free |
There was a problem hiding this comment.
Notice that this runs into the same issue of signerless clients: register, recover and register_or_recover will require a signer, which may not always be present. So using them as a entrypoints to get a Node handle is not a good idea.
There was a problem hiding this comment.
And I just noticed that I overlooked that connect also is requiring the mnemonic, so this API just threw the signerless mode out of the window, which means everybody is running around with root privileges, for no apparent reason, other than simplicity.
We can keep it this way, but we'll want to allow signerless clients at some point, so we will need to bring back in.
2158773 to
ab9ea0a
Compare
Summary
Node::lnurl_auth(request)signs the service'sk1challenge and posts the LUD-04 callback. Per-domain LUD-05 derivation happens internally; callers don't supply any keys.m/138'xpriv is derived once from the BIP39 seed at register/recover/connect time and stored onNodeinZeroizing<Vec<u8>>. The seed itself never persists.m/138'is hardened, so the stored material cannot derive any other wallet key — blast radius is restricted to LNURL-auth identities.disconnect()andDrop for Nodebothtake()the option to scrub eagerly.m/138'/0as the HMAC key (Phoenix / Mutiny / Zeus / BlueWallet form), enabling cross-wallet identity portability at LNURL-auth services. Locked in by a known-vector test.InputType::LnUrlAuth { data }.parse_inputclassifiestag=loginURLs offline (no HTTP fetch). New typesLnUrlAuthRequestDataandLnUrlCallbackStatus.Node(credentials)from both UniFFI and napi surfaces. The crate-private constructor is renamedwith_signer→new(pub(crate)). All bindings expose the same four free functions:register / recover / connect / register_or_recover.register / recover / connect / registerOrRecover, aConfigwrapper (withNetwork,withDeveloperCert), andNode.disconnect()/Node.credentials(). Tests and README rewritten — JS callers no longer manage an external Signer/Handle.Test plan
cargo test -p gl-sdk --lib— 28/28 pass (offline classification, LUD-05 derivation determinism, signature verifies against derived xpub, callback-status JSON parsing, fixed pubkey vector for the BIP39 abandon-mnemonic atexample.com)cargo build -p gl-sdkandcargo build -p gl-sdk-node— both cleanglsdk.Node()raisesValueError(no constructor);register/recover/connect/register_or_recover/parse_input/lnurl_authexposedregister/recover/connect/registerOrRecover/parseInputasync free fns andNode.lnurlAuth(request)exposedtests/test_lnurl.py,tests/test_parse_input.py) — locally blocked by Python 3.14 + coincurve wheel issue unrelated to this branch; should run in CI. New auth coverage: classification-without-HTTP, end-to-end success, deterministic per-domain pubkey, post-disconnect scrub. The two previously-skipped LNURL-pay end-to-end tests are un-skipped via a_bip39_seedhelper that aligns the gl-clientclientsfixture withglsdk.connect's mnemonic-derived seedLnurlParseTest.kt) — 5 new offline LnUrlAuth parse cases; needs on-device or CI rungltestserver)